/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package net.worlds.net;

import java.awt.Color;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.zip.GZIPInputStream;
import net.worlds.WorldsException;
import net.worlds.Chat;
import net.worlds.Main;
import net.worlds.ObjectList;
import net.worlds.TransformDef;
import net.worlds.Util;
import net.worlds.object.Camera;
import net.worlds.object.Character;
import net.worlds.object.GameObject;
import net.worlds.object.NetworkClient;
import net.worlds.object.Part;
import net.worlds.object.Player;
import net.worlds.object.Sound;
import net.worlds.object.Tool;
import net.worlds.object.UIComponent;
import net.worlds.ui.UI;
import org.jbox2d.common.Vec2D;


public class Client implements Runnable{
    private final int lagTime = 0;
    private NetworkClient clientObject;
    private Player player = null;
    private String ip;
    private int port;
    private Socket client;
    private Thread thread;
    private Thread pingThread;
    private DataInputStream in;
    private DataOutputStream out;
    private boolean running = false;
    private boolean disconnected = true;
    private boolean kicked = false;
    private boolean levelLoaded = false;
    private final ByteBuffer outputbuf = ByteBuffer.allocate(1048576);
    private final ByteBuffer inputbuf = ByteBuffer.allocate(1048576);
    private final List<TransformDef> transforms = Collections.synchronizedList(new ArrayList<TransformDef>(1024));
    private final ByteBuffer unfinished = ByteBuffer.allocate(1048576);
    private int currentSize = 0;
    private long lastPingTime = System.currentTimeMillis();
    private long lastPingSendTime;
    private HashMap<Integer, PlayerCursor> cursors = new HashMap<Integer, PlayerCursor>(32);
    public boolean isBuildClient = false;
    private ByteBuffer game;
    public Client(NetworkClient nc, String ip, int port)
    {
        clientObject = nc;
        this.ip = ip;
        this.port = port;
        Main.log("Connecting to "+ip);
        boolean exception = false;
        try {
            client = new Socket(ip, port);
            in = new DataInputStream(client.getInputStream());
            out = new DataOutputStream(client.getOutputStream());
            Main.log("Connection to "+ip+" accepted");
            disconnected = false;
        } catch (Exception ex) {
            Main.getGame().getCurrentWorld().setMessage("Failed to connect to server!");
            ex.printStackTrace();
            exception = true;
            return;
        }
        finally {
            if(!exception)
            {
                running = true;
                thread = new Thread(this, "Client");
                thread.start();
                pingThread = new Thread(new PingThread());
                pingThread.start();
                sendLogin(Main.getLoginName());
            }
        }
    }
    public NetworkClient getClientObject()
    {
        return clientObject;
    }
    public static void start(String ip, int port)
    {
        try {
            NetworkClient c = (NetworkClient) Main.getGame().getMainWorld().createObject("NetworkClient");
            c.setProp("IP", ip, true);
            c.setProp("Port", port, true);
            c.setParent(Main.getGame().getMainWorld().getRootObject());
            c.start();
        } catch (WorldsException ex) {
            ex.printStackTrace();
        }
    }

    public void run() {
        try
        {
            while(!disconnected)
            {
                if(!running || in == null || out == null)
                    break;
                int size = -1;
                int cmd = -1;
                if(unfinished.position() == 0)
                {
                    size = in.readShort();
                    currentSize = size;
                    cmd = in.read();
                }
                processPacket(size, cmd, in);
            }
        }
        catch(Exception ex)
        {
            Main.log("Disconnected from server");
            ex.printStackTrace();
            disconnect();
        }
    }
    private void writeData(ByteBuffer bb)
    {
        if(disconnected)
            return;
        try {
            Thread.sleep(lagTime);
        } catch (InterruptedException ex) {
        }
        Util.checkThread();
        bb.flip();
        try {
            out.writeShort(bb.limit());
            out.write(bb.array(), 0, bb.limit());
            out.flush();
        } catch (IOException ex) {
            Main.log("Error writing data: "+ex);
            disconnect();
            ex.printStackTrace();
        }
    }
    private void processTransforms()
    {
        Main.runInMainThread(new Runnable()
        {
            public void run() {
                synchronized(transforms)
                {
                    Iterator<TransformDef> itr = transforms.iterator();
                    while(itr.hasNext())
                    {
                        itr.next().exec();
                        itr.remove();
                    }
                }
            }
        });
    }
    private void processPacket(int size, int cmd, DataInputStream in) throws IOException
    {
        if(currentSize == 0)
            return;
        inputbuf.clear();
        for(int i = 0; i < currentSize - 1; i++)
        {
            inputbuf.put((byte) in.read());
        }
        inputbuf.flip();
        //else
        {
            //System.err.println("Packet: "+cmd);
            /*System.out.print(cmd+" ");
            while(inputbuf.remaining() > 0)
                System.out.print(inputbuf.get()+" ");
            System.out.println();*/
            switch(cmd)
            {
                case 0:
                    lastPingTime = System.currentTimeMillis();
                    inputbuf.get();
                break;
                case 1:
                    Main.loading = false;
                    Main.getGame().getCurrentWorld().setMessage("Downloading game...");
                    boolean initStatus = getBoolean();
                    /*if(Main.isBuildClient())
                    {
                        if(initStatus)
                            Main.getGame().play(false);
                        else
                            Main.getGame().stop(false);
                    }*/
                break;
                case 4:
                    inputbuf.get();
                    Main.runInMainThread(new Runnable()
                    {
                        public void run() {
                            Main.getView().getPartsTree().create(Main.getGame().getWorld("main").getRootObject());
                            requestCharacter();
                        }
                    });
                break;
                case 5:
                    final String type = getString();
                    final int requestID = inputbuf.getShort();
                    final String worldName = getString();
                    Runnable r = new Runnable()
                    {
                        public void run()
                        {
                            final net.worlds.World createWorld = Main.getGame().getWorld(worldName);
                            if(Main.debug)
                                System.err.println("Received Create: "+requestID+" "+type);
                            GameObject obj = createWorld.createObject(type, requestID, null, false);
                            if(obj instanceof Part)
                                ((Part)obj).isFinished = true;
                        }
                    };
                    Main.runInMainThread(r);

                break;
                case 6:
                    final int id6 = inputbuf.getShort();
                    final String key = getString();
                    final boolean fireEvent = getBoolean();
                    final int propType = inputbuf.get();
                    int objID = -1;
                    Object value = null;
                    if(propType == 0)
                        value = inputbuf.getInt();
                    else if(propType == 1)
                        value = inputbuf.getFloat();
                    else if(propType == 2)
                        value = getBoolean();
                    else if(propType == 3)
                        value = getString();
                    else if(propType == 4)
                        objID = inputbuf.getShort();
                    else if(propType == 5)
                        value = getVec2D();
                    else if(propType == 6)
                        value = getColor();
                    else
                        throw new RuntimeException("Unexpected property type: "+propType);
                    final Object finalValue = value;
                    final int finalID = objID;
                    Main.runInMainThread(new Runnable()
                    {
                        public void run()
                        {
                            final GameObject obj6 = Main.getGame().getByID(id6);
                            try {
                                if(Main.debug)
                                    System.err.println("Received Property: "+id6+" "+obj6+" "+key+" "+(finalValue == null ? finalID : finalValue));
                                if(obj6 == null)
                                    return;
                                if(finalID > -1 && finalValue == null)
                                {
                                    if(finalID == Short.MIN_VALUE)
                                        obj6.setProp(key, null, fireEvent, false);
                                    else
                                        obj6.setProp(key, obj6.world.game.getByID(finalID), fireEvent, false);
                                }
                                else
                                    obj6.setProp(key, finalValue, fireEvent, false);
                            } catch (WorldsException ex) {
                                throw new RuntimeException(ex.getError());
                            }
                        }
                    });
                break;
                case 7:
                    boolean done = false;
                    String world = getString();
                    if(!Main.getGame().getCurrentWorld().name.equals(world))
                        break;
                    synchronized(transforms)
                    {
                        while(done == false)
                        {
                            final int id = inputbuf.getShort();
                            if(id == Short.MIN_VALUE)
                            {
                                done = true;
                                break;
                            }
                            final int hash = inputbuf.getShort();
                            final Vec2D pos = getVec2D();
                            final float rotation = inputbuf.getFloat();
                            TransformDef td = new TransformDef();
                            td.id = id;
                            td.hash = hash;
                            td.pos = pos;
                            td.rotation = rotation;
                                transforms.add(td);
                        }
                    }
                    processTransforms();
                break;
                case 8:
                    boolean done8 = false;
                    String world8 = getString();
                    if(!Main.getGame().getCurrentWorld().name.equals(world8))
                        break;
                    synchronized(transforms)
                    {
                        while(done8 == false)
                        {
                            final int id = inputbuf.getShort();
                            if(id == Short.MIN_VALUE)
                            {
                                done8 = true;
                                break;
                            }
                            final int hash = inputbuf.getShort();
                            final Vec2D vel = getVec2D();
                            final float rvel = inputbuf.getFloat();
                            TransformDef td = new TransformDef();
                            td.id = id;
                            td.hash = hash;
                            td.vel = vel;
                            td.rvel = rvel;
                            transforms.add(td);
                        }
                    }
                    processTransforms();
                break;
                case 10:
                    final int id10 = inputbuf.getShort();
                    Main.runInMainThread(new Runnable()
                    {
                        public void run() {
                            GameObject obj10 = Main.getGame().getByID(id10);
                            System.err.println("Set character: "+id10+" "+obj10);
                            if(obj10 != null && obj10 instanceof Player)
                                try {
                                    player = (Player) obj10;
                                    Main.getGame().getMainWorld().getService("NetworkClient").setProp("LocalPlayer", (Player) obj10, true);
                                    if(obj10.getProp("Character") != null && obj10.getProp("Character") instanceof net.worlds.object.Character && obj10.getObjectProp("Character").hasChild("Body") && obj10.getObjectProp("Character").getChild("Body") instanceof Part)
                                        ((Part)((net.worlds.object.Character)obj10.getObjectProp("Character")).getChild("Body")).isRemoteCharacter = false;
                                    if(obj10.getChild("Camera") != null && obj10.getChild("Camera") instanceof Camera)
                                    {
                                        Main.getGame().currentCamera = (Camera) obj10.getChild("Camera");
                                        Main.getView().setViewPropertiesFromCamera(Main.getGame().currentCamera);
                                    }
                                } catch (WorldsException ex) {
                                    ex.printStackTrace();
                                }
                            Main.getGame().play(false);
                            Main.getGame().getCurrentWorld().setMessage(null);
                            //Main.getActiveWorld().play(false);
                            levelLoaded = true;
                            if(isBuildClient)
                                try
                                {
                                    Main.getGame().currentCamera.zoomToCenter();
                                }
                                catch(Exception ex)
                                {
                                    ex.printStackTrace();
                                }
                        }
                    });
                break;
                case 13:
                    final int status = inputbuf.get();
                    if(status == 0)
                        Main.getGame().stop(false);
                    else
                        Main.getGame().play(false);
                break;
                case 15:
                    final int inUseID = inputbuf.getShort();
                    final boolean inUseStatus = getBoolean();
                    Main.runInMainThread(new Runnable()
                    {
                        public void run() {
                            GameObject inUseObj = getObject(inUseID);
                            if(inUseObj == null)
                                return;
                            inUseObj.inUse = inUseStatus;
                        }
                    });
                break;
                case 16:
                    String text = getString();
                    System.out.println("[SERVER] "+text);
                break;
                case 17:
                    /*int playerid = inputbuf.getShort();
                    Vec2D playerpos = getVec2D();
                    if(cursors.get(playerid) == null)
                    {
                        PlayerCursor c = new PlayerCursor();
                        c.player = (Player) Main.getActiveWorld().getByID(playerid);
                        c.pos = playerpos;
                        if(c.player != null)
                            cursors.put(playerid, c);
                    }
                    else
                    {
                        PlayerCursor c = cursors.get(playerid);
                        c.pos = playerpos;
                        if(playerpos.getX() == 0.0)
                        {
                            cursors.remove(playerid);
                        }
                    }*/
                break;
                case 19:
                        final int oldID = inputbuf.getShort();
                        final int newID = inputbuf.getShort();
                        Main.runInMainThread(new Runnable()
                        {
                            public void run() {
                                GameObject changeObj = Main.getGame().getByID(oldID);
                                System.err.println("Change ID: "+oldID+" "+newID);
                                if(changeObj != null)
                                {
                                    changeObj.mine = false;
                                    changeObj.setID(newID, false);
                                }
                                else
                                    Main.log("obj == null");
                            }
                        });
                break;
                case 20:
                    final int finishID = inputbuf.getShort();
                    Main.runInMainThread(new Runnable()
                    {
                        public void run() {
                            GameObject finishObj = getObject(finishID);
                            if(finishObj instanceof Part)
                                ((Part)finishObj).isFinished = true;
                        }
                    });
                break;
                case 21:
                    Color c = getColor();
                    String msg = getString();
                    Chat.addChat(c, msg);
                break;
                case 22:
                    final int moveID = inputbuf.getShort();
                    final int action = inputbuf.get();
                    Main.runInMainThread(new Runnable()
                    {
                        public void run() {
                            GameObject obj = getObject(moveID);
                            try
                            {
                                if(obj instanceof Part)
                                {
                                    if(action == 0)
                                        UI.moveToBack((Part)obj, false);
                                    else
                                        UI.moveToFront((Part)obj, false);
                                }
                            }
                            catch(WorldsException ex)
                            {
                                ex.printStackTrace();
                            }
                        }
                    });
                break;
                case 23:
                    int mtype = inputbuf.get();
                    String message = getString();
                    if(mtype == 0)
                    {
                        if(message.equals(""))
                            Main.getGame().getCurrentWorld().setMessage(null);
                        else
                            Main.getGame().getCurrentWorld().setMessage(message);
                    }
                    else if (mtype == 1 && Main.getLocalPlayer() != null && Main.getLocalPlayer().getPlayer() != null) {
                            if (message.equals("")) {
                                 Main.getLocalPlayer().getPlayer().setMessage(null);
                            } else {
                                 Main.getLocalPlayer().getPlayer().setMessage(message);
                            }
                        }
                break;
                case 24:
                    String kickMessage = getString();
                    try {
                        Main.setLocalPlayer(null);
                    } catch (WorldsException ex) {
                        ex.printStackTrace();
                    }
                    Main.loading = true;
                    Main.getGame().getCurrentWorld().setMessage(kickMessage);
                    kicked = true;
                    disconnect();
                break;
            case 27:
                final int characterID = inputbuf.getShort();
                final int toolID = inputbuf.getShort();
                Main.runInMainThread(new Runnable() {

                    public void run() {
                        try {
                            net.worlds.object.Character character = (Character) Main.getGame().getByID(characterID);
                            if(toolID == -1)
                            {
                                character.net_selectedTool = null;
                                character.net_armRotation = false;
                            }
                            else
                            {
                                character.net_selectedTool = (Tool) Main.getGame().getByID(toolID);
                                character.net_armRotation = true;
                            }
                        } catch (Exception ex) {
                            ex.printStackTrace();
                        }
                    }
                });
            break;
            case 28:
                final int armcharacterID = inputbuf.getShort();
                final double armRotation = (double) inputbuf.getFloat();
                Main.runInMainThread(new Runnable() {
                    public void run() {
                        try {
                            net.worlds.object.Character character = (Character) Main.getGame().getByID(armcharacterID);
                            if(character != null)
                                character.net_armAngle = armRotation;
                        } catch (Exception ex) {
                            ex.printStackTrace();
                        }
                    }
                });
            break;
            case 31:
                final int playID = inputbuf.getShort();
                Main.runInMainThread(new Runnable() {
                    public void run() {
                        GameObject obj = Main.getGame().getByID(playID);
                    try {
                        ((Sound)obj).play();
                    } catch (WorldsException ex) {
                    }
                    }
                });
            break;
            case 32:
                final int stopID = inputbuf.getShort();
                Main.runInMainThread(new Runnable() {
                    public void run() {
                        GameObject obj = Main.getGame().getByID(stopID);
                        ((Sound)obj).stop();
                    }
                });
            break;
            case 101:
                int len = inputbuf.getInt();
                game = ByteBuffer.allocate(len);
                game.clear();
            break;
            case 102:
                int gsize = inputbuf.getShort();
                byte[] payload = new byte[1020];
                inputbuf.get(payload, 0, 1020);
                for(int i = 0; i < gsize; i++)
                {
                    game.put(payload[i]);
                }
            break;
            case 103:
                int length = inputbuf.getInt();
                byte[] arr = game.array();
               
                ByteArrayInputStream bIn = new ByteArrayInputStream(arr);
                GZIPInputStream gzIn = new GZIPInputStream(bIn);
                byte[] levelBytes = new byte[length];
                int len2 = -1;
                int offset = 0;
                while((len2 = gzIn.read(levelBytes, offset, levelBytes.length - offset)) != 0)
                {
                    offset += len2;
                }
                gzIn.close();
                bIn.close();
                final String data = new String(levelBytes);
                Main.runInMainThread(new Runnable()
                {
                    public void run() {
                        Main.setLoading(true);
                        Main.loadGame(data, false);
                        Main.finishLoading(false);
                    }
                });
            break;
            }
        }
        try {
            Thread.sleep(lagTime);
        } catch (InterruptedException ex) {
        }
    }
    public void sendLogin(String name)
    {
        synchronized(outputbuf)
        {
            Main.getGame().getCurrentWorld().setMessage("Logging in...");
            outputbuf.clear();
            outputbuf.put((byte) 1);
            writeString(name);
            writeData(outputbuf);
        }
    }
    public void sendPing()
    {
        synchronized(outputbuf)
        {
            outputbuf.clear();
            outputbuf.put((byte) 0);
            writeData(outputbuf);
        }
    }
    public void sendPosition(Part p)
    {
        synchronized(outputbuf)
        {
            outputbuf.clear();
            outputbuf.put((byte) 11);
            writeVec2D(p.getPosition());
            writeVec2D(p.getVelocity());
            writeData(outputbuf);
        }
    }
    public void requestCharacter()
    {
        Main.getGame().getCurrentWorld().setMessage("Requesting character...");
        synchronized(outputbuf)
        {
            outputbuf.clear();
            outputbuf.put((byte)9);
            writeData(outputbuf);
        }
    }
    public void sendObjectRecursive(GameObject obj)
    {
        if(!levelLoaded || !obj.isSendable)
            return;
        synchronized(outputbuf)
        {
            List<GameObject> children = obj.getChildren();
            synchronized(children)
            {
                for(GameObject c : children)
                {
                    if(!(c instanceof NetworkClient))
                    {
                        sendObject(c);
                        sendObjectRecursive(c);
                    }
                }
            }
        }
    }
    public void sendObject(GameObject obj)
    {
        if(!levelLoaded || !obj.isSendable || Util.isLocal(obj.getType()))
            return;
        synchronized(outputbuf)
        {
            sendObjectCreate(obj);
            try {
                for(String k : obj.getAllProperties().keySet())
                {
                        boolean fireEvent = !k.equals("Parent");
                        if(obj.getProp(k) != null)
                            sendProp(obj, k, obj.getProp(k), fireEvent);
                }
                if(obj.getParent() != null)
                sendProp(obj, "Parent", obj.getParent(), true);
            } catch (WorldsException ex) {
                ex.printStackTrace();
            }
        }
    }
    public void sendObjectCreate(final GameObject obj)
    {
        obj.mine = true;
        if(!levelLoaded || !obj.isSendable || Util.isLocal(obj.getType()))
            return;
        synchronized(outputbuf)
        {
            outputbuf.clear();
            outputbuf.put((byte) 5);
            writeString(obj.getType());
            outputbuf.putShort((short) obj.getID());
            writeString(obj.world.name);
            writeData(outputbuf);
        }
    }
    public void sendProp(GameObject obj, String name, Object value, boolean fireEvent)
    {
        if(!levelLoaded || !obj.isSendable || Util.isLocal(obj.getType()))
            return;
        try {
            if (name.equals("Type") || (name.equals("Name") && value.equals(obj.getType())) || (obj instanceof Part && obj.getParent() instanceof net.worlds.object.Character)) {
                return;
            }
        } catch (WorldsException ex) {
            ex.printStackTrace();
        }
        synchronized(outputbuf)
        {
            outputbuf.clear();
            outputbuf.put((byte) 6);
            outputbuf.putShort((short) obj.getID());
            writeBoolean(obj.mine);
            writeString(name);
            writeBoolean(fireEvent);
            if(value instanceof Integer)
            {
                outputbuf.put((byte) 0);
                outputbuf.putInt((Integer)value);
            }
            else if(value instanceof Float)
            {
                outputbuf.put((byte) 1);
                outputbuf.putFloat((Float) value);
            }
            else if(value instanceof Boolean)
            {
                outputbuf.put((byte) 2);
                writeBoolean((Boolean) value);
            }
            else if(value instanceof String)
            {
                outputbuf.put((byte) 3);
                writeString((String) value);
            }
            else if(value instanceof GameObject)
            {
                outputbuf.put((byte) 4);
                writeObject((GameObject) value);
            }
            else if(value instanceof Vec2D)
            {
                outputbuf.put((byte) 5);
                writeVec2D((Vec2D) value);
            }
            else if(value instanceof Color)
            {
                outputbuf.put((byte) 6);
                writeColor((Color) value);
            }
            else if(value == null && ObjectList.getPropDef(obj.getClass().getSimpleName(), name).type == GameObject.class)
            {
                outputbuf.put((byte) 4);
                writeObject(null);
            }
            else
                throw new RuntimeException("Unexpected property type: "+value.getClass());
            writeData(outputbuf);
        }
    }
    public void sendWorldStatus(int status)
    {
        synchronized(outputbuf)
        {
            outputbuf.clear();
            outputbuf.put((byte) 13);
            outputbuf.put((byte) status);
            writeData(outputbuf);
        }
    }
    public void sendAddJoint(Vec2D pos, int type)
    {
        synchronized(outputbuf)
        {
            outputbuf.clear();
            outputbuf.put((byte) 14);
            writeVec2D(pos);
            outputbuf.put((byte)type);
            writeData(outputbuf);
        }
    }
    public void sendInUse(GameObject obj, boolean status)
    {
        synchronized(outputbuf)
        {
            outputbuf.clear();
            outputbuf.put((byte)15);
            writeObject(obj);
            writeBoolean(status);
            writeData(outputbuf);
        }
    }
    public void sendCursorPosition(Vec2D pos)
    {
        synchronized(outputbuf)
        {
            outputbuf.clear();
            outputbuf.put((byte)17);
            writeVec2D(pos);
            writeData(outputbuf);
        }
    }
    public void sendScript(String source)
    {
        synchronized(outputbuf)
        {
            outputbuf.clear();
            outputbuf.put((byte)18);
            writeString(source);
            writeData(outputbuf);
        }
    }
    public void sendIDChange(int oldID, int newID)
    {
        synchronized(outputbuf)
        {
            outputbuf.clear();
            outputbuf.put((byte)19);
            outputbuf.putShort((short)oldID);
            outputbuf.putShort((short)newID);
            writeData(outputbuf);
        }
    }
    public void sendChat(String msg)
    {
        synchronized(outputbuf)
        {
            outputbuf.clear();
            outputbuf.put((byte)21);
            writeString(msg);
            writeData(outputbuf);
        }
    }
    public void sendMoveTo(Part p, int action)
    {
        synchronized(outputbuf)
        {
            outputbuf.clear();
            outputbuf.put((byte)22);
            outputbuf.putShort((short)p.getID());
            outputbuf.put((byte)action);
            writeData(outputbuf);
        }
    }
    public void sendToolActive(Tool t, boolean active)
    {
        synchronized(outputbuf)
        {
            outputbuf.clear();
            outputbuf.put((byte)23);
            outputbuf.putShort((short) t.getID());
            writeBoolean(active);
            writeData(outputbuf);
        }
    }
    public void sendToolMouse(Tool t, int action, int btn, Vec2D position)
    {
        synchronized(outputbuf)
        {
            outputbuf.clear();
            outputbuf.put((byte) 24);
            outputbuf.putShort((short)t.getID());
            outputbuf.put((byte) action);
            outputbuf.put((byte) btn);
            writeVec2D(position);
            writeData(outputbuf);
        }
    }
    public void sendToolKey(Tool t, int action, String k)
    {
        synchronized(outputbuf)
        {
            outputbuf.clear();
            outputbuf.put(((byte)25));
            outputbuf.putShort((short)t.getID());
            outputbuf.put((byte)action);
            writeString(k);
            writeData(outputbuf);
        }
    }
    public void sendClick(Part p, int btn)
    {
        synchronized(outputbuf)
        {
            outputbuf.clear();
            outputbuf.put((byte)26);
            outputbuf.putShort((short) p.getID());
            outputbuf.put((byte) btn);
            writeData(outputbuf);
        }
    }
    public void sendSelectedTool(Tool t)
    {
        synchronized(outputbuf)
        {
            outputbuf.clear();
            outputbuf.put((byte)27);
            if(t == null)
                outputbuf.putShort((short) -1);
            else
                outputbuf.putShort((short) t.getID());
            writeData(outputbuf);
        }
    }
    public void sendArmRotation(double rotation)
    {
        synchronized(outputbuf)
        {
            outputbuf.clear();
            outputbuf.put((byte)28);
            outputbuf.putFloat((float)rotation);
            writeData(outputbuf);
        }
    }
    public void sendButtonClick(UIComponent p, int btn, int x, int y)
    {
        synchronized(outputbuf)
        {
            outputbuf.clear();
            outputbuf.put((byte)30);
            outputbuf.put((byte) btn);
            outputbuf.putShort((short) p.getID());
            outputbuf.putShort((short)x);
            outputbuf.putShort((short)y);
            writeData(outputbuf);
        }
    }
    public void disconnect()
    {
        if(disconnected)
            return;
        disconnected = true;
        if(!kicked)
            Main.getGame().getCurrentWorld().setMessage("Disconnected from server!");
        running = false;
        if(pingThread != null)
            pingThread.stop();
        try {
            if(!client.isClosed())
                client.close();
            Main.setClient(null);
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
    private void writeBoolean(boolean v)
    {
        if(v)
            outputbuf.put((byte)1);
        else
            outputbuf.put((byte)0);
    }
    private boolean getBoolean()
    {
        if(inputbuf.get() == 1)
            return true;
        else
            return false;
    }
    private void writeString(String s)
    {
        outputbuf.putShort((short) s.length());
        outputbuf.put(s.getBytes());
    }
    private String getString()
    {
        int len = inputbuf.getShort();
        byte[] bytes = new byte[len];
        inputbuf.get(bytes, 0, len);
        return new String(bytes);
    }
    private String getLongString()
    {
        int len = inputbuf.getInt();
        byte[] bytes = new byte[len];
        inputbuf.get(bytes, 0, len);
        return new String(bytes);
    }
    private void writeVec2D(Vec2D v)
    {
        outputbuf.putFloat((float) v.x);
        outputbuf.putFloat((float) v.y);
    }
    private Vec2D getVec2D()
    {

        return new Vec2D(inputbuf.getFloat(), inputbuf.getFloat());
    }
    private void writeColor(Color c)
    {
        outputbuf.put((byte) c.getRed());
        outputbuf.put((byte) c.getGreen());
        outputbuf.put((byte) c.getBlue());
    }
    private Color getColor()
    {
        int r = Util.parseUnsignedByte(inputbuf.get());
        int g = Util.parseUnsignedByte(inputbuf.get());
        int b = Util.parseUnsignedByte(inputbuf.get());
        return new Color(r, g, b);
    }
    private void writeObject(GameObject obj)
    {
        if(obj == null)
            outputbuf.putShort(Short.MIN_VALUE);
        else if(obj.mine)
            outputbuf.putShort((short) -obj.getID());
        else
            outputbuf.putShort((short)obj.getID());
    }
    private GameObject getObject(int id)
    {
        Util.checkThread();
        if(id == Short.MIN_VALUE)
            return null;
        else
            return Main.getGame().getByID(id);
    }
    private class PingThread implements Runnable
    {

        public void run() {
            while(true)
            {
                Main.runInMainThread(new Runnable()
                {
                    public void run() {
                        sendPing();
                    }
                });
                try {
                    Thread.sleep(30000);
                } catch (InterruptedException ex) {
                }
            }
        }
    }
    public HashMap<Integer, PlayerCursor> getCursors()
    {
        return cursors;
    }
}
